前兩講聊到 Unit Tests 要寫什麼、用 Stub 和 Mock 來處理 Side Effect 的情況。今天則讓我們來看看前端和後端有什麼重點的 Unit Tests 可以測試,畢竟寫測試也是需要花時間的,我們不太可能把所有的程式碼都覆蓋上 Unit Tests,在時間有限的情況下應該要挑重點來做。
我認為前後端都有的工具類(Utility)Function 最適合做單元測試,因為大部分的工具類 Function 都不需要特別考慮 Side Effect,有單純的輸入以及輸出。
如果沒有 Side Effect,就意味著我們不需要 Stub 及 Mock 這兩招來應對外部環境的影響,可以將心思都花在輸入的資料有哪些、期望的輸出是什麼。如果有 Side Effect,特別是在用到 Mock 時,撰寫測項的時間成本就高得多,因為需要考慮如何和 Mock 的環境互動。
因此,實務上我會盡可能的把所謂工具類的 Function 從有 Side Effect 的大 Function 裡面拆出來,特別針對此做單元測試。
那麼怎麼樣的 Function 是工具類的呢?
function deepClone(obj) {
return JSON.parse(JSON.stringify(obj));
}
function calculateAverage(numbers) {
return numbers.reduce((sum, value) => sum + value, 0) / numbers.length;
}
function convertSpeed(speedInMs, targetUnit) {
switch (targetUnit) {
case 'mph':
return speedInMs * 2.23694;
case 'kph':
return speedInMs * 3.6;
default:
throw new Error('Invalid target unit');
}
}
以上如資料轉換、計算類,包含明確條件(if else / switch case),但是輸入和輸出都是可控、可預期的,就非常適合寫 Unit Tests。
以上 Function 的邏輯都算是簡單的,而 Unit Testing 更適用在邏輯再複雜一些,需要花點心思才能釐清的 Function 上,寫完了不但可以預防之後修改程式碼不小心改壞,也能夠當作理解這個 Function 的使用手冊。
除了適用前後端的工具類 Function,如果聚焦在前端上,我認為 UI 的 State 和 Event 類最適合寫 Unit Tests。
其中 Event 類指的是 User 和 UI 的互動,例如點擊 Button、拖曳物件等等的行為,輸入就是用戶的的動作,而輸出則是產生的 Event。
至於 UI 的 State 則是我接下來舉的例子中要聊到的:
例如一個複雜的表單,其中的某個元件狀態會根據其他元件而被影響,我們就可以撰寫 Unit Tests 來羅列輸入的元件狀態,可以怎麼影響到目標的元件狀態。
用一個簡化的例子來說,我們有一個可以提交意見的表單,只有一個輸入的 Input(Textarea)和提交的 Button。
如果 Input 沒有內容,則提交 Button 就不能按;而且輸入也不能超過 30 個字,一旦超過則提交 Button 也會被 Disable。
*Feedback 表單及 Button 狀態示意圖
以下是用 React 所實作的 FeedbackForm Component,其中狀態轉換只用簡單的邏輯來判斷 Input 的字數,進而改變 Button 是否 Disable。
import React, { useState } from 'react';
function FeedbackForm() {
const [inputValue, setInputValue] = useState('');
const isButtonDisabled = inputValue.length === 0 || inputValue.length > 30;
return (
<div>
<textarea value={inputValue} onChange={(e) => setInputValue(e.target.value)} />
<button disabled={isButtonDisabled}></button>
</div>
);
}
export default FeedbackForm;
那麼,我們要怎麼寫單元測試呢?
我們可以 Render 一個 FeedbackForm,將 InputElement 塞入不同的值,參照前兩講提過的 Boundary Testing,將一些邊界的數值納入測試。
由於 InputElement 沒有任何內容和超過 30 字的情況會使得 Button Disabled,這邊的 Boundaries 就可以丟入 0, 1, 30, 31
的邊界值來測試。
test.each([
{ input: '', expectedDisabled: true},
{ input: 'a', expectedDisabled: false},
{ input: 'a'.repeat(30), expectedDisabled: false},
{ input: 'a'.repeat(31), expectedDisabled: true},
])('With input "$input", button disabled state should be $expectedDisabled', ({input, expectedDisabled}) => {
render(<FeedbackForm />);
const inputElement = screen.getByRole('textbox');
const buttonElement = screen.getByRole('button');
fireEvent.change(inputElement, { target: { value: input } });
expect(buttonElement).toBeDisabled(expectedDisabled);
});
如此一來,我們就將 Button 因為不同輸入而有不同狀態的測試完成了。